iT邦幫忙

2026 iThome 鐵人賽

DAY 15
0
自我挑戰組

Laravel 讀碼源系列 第 15

解釋產品管理列表的修改前端code

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20260824/20119035uzApZUn5CF.png

https://ithelp.ithome.com.tw/upload/images/20260824/201190352TzyyVqNIW.png

https://ithelp.ithome.com.tw/upload/images/20260824/201190359nNxKGL0D8.png

解釋程式碼
這份 edit.blade.php 是一個 Laravel Blade 編輯產品頁面,功能包括:

  • 顯示產品目前資料。
  • 編輯產品標題、說明、價格與庫存。
  • 顯示產品圖片。
  • 透過 AJAX 刪除指定圖片。
  • 顯示表單驗證錯誤與成功訊息。
  • 使用 CSRF 保護與 Laravel 的路由機制。

Blade 是 Laravel 的樣板引擎,{{ }} 用來輸出資料,@if@foreach@error 則是 Blade 指令。cite

1. HTML 文件基本結構

<!DOCTYPE html>
<html lang="zh-TW">

這表示:

  • 使用 HTML5。
  • 網頁語言設定為繁體中文台灣:zh-TW
<meta charset="UTF-8">

使用 UTF-8 編碼,因此可以正確顯示中文、日文與表情符號。

<meta name="viewport" content="width=device-width, initial-scale=1.0">

讓頁面適應手機、平板與桌面螢幕。

<meta name="csrf-token" content="{{ csrf_token() }}">

這一行把 Laravel 產生的 CSRF Token 放進 HTML 的 <meta> 標籤,稍後 JavaScript 刪除圖片時會讀取它。

CSRF 是 Laravel 用來防止偽造請求的安全機制;只要是修改或刪除資料的請求,就應該附帶有效 Token。cite

2. 載入 jQuery

<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>

這裡載入 jQuery 3.6.0,主要用於後面的 AJAX:

$.ajax({
    ...
});

如果不想依賴 jQuery,也可以改用原生 fetch()

3. CSS 樣式區域

<style> 裡面的內容負責整個頁面的外觀。

全域設定

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

這會:

  • 移除所有元素的預設外距與內距。
  • 使用 border-box,讓元素的寬度計算比較容易控制。

頁面背景

body {
    font-family: 'Microsoft JhengHei', sans-serif;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    min-height: 100vh;
    padding: 40px 20px;
}

設定:

  • 使用微軟正黑體。
  • 紫藍色漸層背景。
  • 最小高度為整個視窗高度。
  • 上下左右保留空間。

主要容器

.container {
    max-width: 900px;
    margin: 0 auto;
}

.container 最大寬度為 900px,並透過:

margin: 0 auto;

讓內容水平置中。

返回按鈕

.back-btn {
    display: inline-block;
    background: white;
    color: #667eea;
    ...
}

這是「返回產品列表」連結的樣式。

.back-btn:hover {
    transform: translateY(-2px);
    box-shadow: 0 5px 15px rgba(255, 255, 255, 0.3);
}

滑鼠移上去時,按鈕會向上移動 2px,並出現陰影。

表單卡片

.form-card {
    background: white;
    border-radius: 20px;
    padding: 40px;
    box-shadow: 0 10px 40px rgba(0,0,0,0.2);
}

白色卡片是整個編輯表單的主要容器,具有:

  • 白色背景。
  • 圓角。
  • 內距。
  • 陰影效果。

輸入欄位

.form-control {
    width: 100%;
    padding: 12px 16px;
    border: 2px solid #ecf0f1;
    border-radius: 10px;
}

所有輸入框與文字區域都會套用這個樣式。

.form-control:focus {
    outline: none;
    border-color: #667eea;
    box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}

當使用者點擊輸入框時,邊框變成紫藍色並出現淡淡的外框。

4. Laravel 路由連結

<a href="{{ route('admin.products.card') }}" class="back-btn">
    ← 返回產品列表
</a>
route('admin.products.card')

會根據 Laravel 中命名為 admin.products.card 的路由產生 URL。

例如路由可能是:

Route::get('/admin/products', [ProductController::class, 'card'])
    ->name('admin.products.card');

這樣 Blade 就不用直接寫死 /admin/products,未來修改路由時較容易維護。

5. 顯示產品 ID

<span class="product-id-badge">#{{ $product->id }}</span>

假設控制器傳入的 $product->id15,瀏覽器上會顯示:

#15

$product 通常是從資料庫取得的 Eloquent Model,例如:

$product = Product::with('images')->findOrFail($id);

6. 顯示驗證錯誤

@if ($errors->any())
    <div class="alert alert-danger">
        <strong>請修正以下錯誤:</strong>
        <ul>
            @foreach ($errors->all() as $error)
                <li>{{ $error }}</li>
            @endforeach
        </ul>
    </div>
@endif

這段程式的作用是:

  1. 檢查是否有任何驗證錯誤。
  2. 如果有錯誤,顯示錯誤區塊。
  3. 使用 @foreach 列出所有錯誤訊息。

例如控制器可能有以下驗證:

$request->validate([
    'title' => ['required', 'string', 'max:255'],
    'price' => ['required', 'integer', 'min:0'],
    'quantity' => ['required', 'integer', 'min:0'],
]);

如果使用者沒有填寫產品標題,頁面可能顯示:

請修正以下錯誤:
The title field is required.

7. 顯示成功訊息

@if (session('success'))
    <div class="alert alert-success">
        {{ session('success') }}
    </div>
@endif

控制器可以使用:

return redirect()
    ->back()
    ->with('success', '產品更新成功');

之後,Blade 會讀取 Session 中的 success 訊息並顯示。

8. 顯示產品圖片

@if($product->images && $product->images->count() > 0)

這裡檢查兩件事:

  • $product->images 是否存在。
  • 圖片數量是否大於 0。

這表示 Product Model 很可能有以下關聯:

public function images()
{
    return $this->hasMany(ProductImage::class);
}

如果有圖片,就執行:

@foreach($product->images as $image)

逐一讀取每張圖片。

圖片網址

<img src="/show-image.php?path={{ urlencode($image->path) }}" alt="產品圖片">

假設:

$image->path = 'products/abc.jpg';

urlencode() 後會放入網址參數:

/show-image.php?path=products%2Fabc.jpg

show-image.php 應該負責:

  1. 接收圖片路徑。
  2. 找到圖片檔案。
  3. 設定正確的 Content-Type。
  4. 將圖片輸出給瀏覽器。

使用 urlencode() 可以避免檔案路徑中的特殊字元破壞 URL。

沒有圖片時

@else
    <div class="no-images">
        <p>📭 尚無圖片</p>
    </div>
@endif

如果產品沒有任何圖片,就顯示「尚無圖片」。

9. 刪除圖片按鈕

<button class="delete-image" onclick="deleteImage({{ $image->id }})">
    ×
</button>

假設圖片 ID 是 8,實際產生的 HTML 類似:

<button onclick="deleteImage(8)">×</button>

使用者按下按鈕後,會呼叫:

deleteImage(8);

這個按鈕位於圖片右上角,CSS 使用:

position: absolute;
top: 5px;
right: 5px;

因此會疊在圖片上方。

10. 編輯產品表單

<form method="POST"
      action="{{ route('admin.products.update', $product->id) }}">

表單使用 POST 傳送資料,網址由命名路由產生。

Laravel 的命名路由可能如下:

Route::put('/admin/products/{product}',
    [ProductController::class, 'update'])
    ->name('admin.products.update');

CSRF 保護

@csrf

這會產生類似以下的隱藏欄位:

<input type="hidden" name="_token" value="...">

它用來驗證請求是否來自合法的網站表單。

PUT 方法偽裝

@method('PUT')

HTML 表單原生只支援 GETPOST,不直接支援 PUTPATCHDELETE。Laravel 會利用隱藏欄位 _method 來偽裝 HTTP 方法。cite

實際上會產生:

<input type="hidden" name="_method" value="PUT">

因此,雖然表單標示:

method="POST"

Laravel 最後會把它視為:

PUT /admin/products/{id}

11. 產品標題欄位

<input type="text"
       name="title"
       class="form-control"
       value="{{ old('title', $product->title) }}"
       required>

name="title"

控制器可以透過:

$request->title

或:

$request->input('title')

取得內容。

old('title', $product->title)

這段很重要:

old('title', $product->title)

意思是:

  • 如果表單驗證失敗,優先顯示使用者剛才輸入的內容。
  • 如果沒有舊輸入資料,顯示資料庫中的 $product->title

例如資料庫標題是:

初級鋼琴課程

使用者修改成:

成人鋼琴入門課程

但其他欄位驗證失敗,重新載入頁面時仍會保留使用者輸入的新版標題。

required

required

這是瀏覽器端的基本驗證,要求標題不可空白。不過後端仍然必須再次驗證,因為瀏覽器驗證可以被繞過。

12. 欄位錯誤訊息

@error('title')
    <p class="error-text">{{ $message }}</p>
@enderror

這表示如果 title 欄位有錯誤,就顯示該欄位的錯誤訊息。

Laravel 的 @error 指令可以快速檢查特定欄位是否有驗證錯誤。cite

其他欄位也使用相同方式:

@error('content')
@error('price')
@error('quantity')

13. 產品說明欄位

<textarea name="content" class="form-control">
{{ old('content', $product->content) }}
</textarea>

這是多行文字輸入框,適合輸入:

  • 產品介紹。
  • 課程特色。
  • 產品規格。
  • 使用方式。
  • 注意事項。
textarea.form-control {
    min-height: 120px;
    resize: vertical;
}

表示文字區域:

  • 最小高度為 120px。
  • 使用者只能垂直調整高度。

需要注意的是,這個欄位的內容直接放在 <textarea> 開始與結束標籤之間,而不是放在 value 屬性中。

14. 價格欄位

<input type="number"
       name="price"
       class="form-control"
       value="{{ old('price', $product->price) }}"
       min="0"
       step="1"
       required>

這個欄位的設定如下:

屬性 作用
type="number" 顯示數字輸入欄位
name="price" 欄位名稱為 price
min="0" 不允許輸入負數
step="1" 以整數為單位
required 必須填寫

不過 minsteprequired 只是前端限制,後端仍應驗證:

'price' => ['required', 'integer', 'min:0']

15. 庫存數量欄位

<input type="number"
       name="quantity"
       class="form-control"
       value="{{ old('quantity', $product->quantity) }}"
       min="0"
       required>

這是庫存欄位,允許輸入 0,代表目前沒有庫存。

後端可以使用:

'quantity' => ['required', 'integer', 'min:0']

確保數量是非負整數。

16. 儲存按鈕

<button type="submit" class="btn-submit">
    💾 儲存變更
</button>

按下後,瀏覽器會送出表單,Laravel 將請求交給產品更新控制器,例如:

public function update(Request $request, Product $product)
{
    $validated = $request->validate([
        'title' => ['required', 'string', 'max:255'],
        'content' => ['nullable', 'string'],
        'price' => ['required', 'integer', 'min:0'],
        'quantity' => ['required', 'integer', 'min:0'],
    ]);

    $product->update($validated);

    return redirect()
        ->route('admin.products.edit', $product)
        ->with('success', '產品更新成功');
}

17. deleteImage() JavaScript 函式

function deleteImage(imageId) {

這個函式接收圖片 ID。

例如:

deleteImage(8);

表示要刪除 ID 為 8 的圖片。

確認視窗

if (!confirm('確定要刪除這張圖片嗎?')) {
    return;
}

confirm() 會顯示確認視窗。

  • 按「確定」:繼續刪除。
  • 按「取消」:執行 return,停止函式。

這可以避免使用者誤刪圖片。

AJAX DELETE 請求

$.ajax({
    url: '/admin/products/images/' + imageId,
    method: 'DELETE',

假設 imageId8,請求網址會變成:

/admin/products/images/8

HTTP 方法是:

DELETE

Laravel 路由可能是:

Route::delete(
    '/admin/products/images/{image}',
    [ProductImageController::class, 'destroy']
)->name('admin.products.images.destroy');

傳送 CSRF Token

headers: {
    'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
}

這段程式:

  1. 找到 <meta name="csrf-token">
  2. 讀取它的 content
  3. 放到 X-CSRF-TOKEN HTTP Header。
  4. Laravel 便能驗證這個 AJAX 刪除請求。

如果忘記傳送 Token,Laravel 常會回傳 419 Page Expired。cite

成功處理

success: function(response) {
    location.reload();
}

刪除成功後重新載入整個頁面,讓已刪除的圖片不再顯示。

失敗處理

error: function(xhr) {
    alert('刪除失敗,請稍後再試');
}

如果伺服器回傳錯誤,就顯示提示訊息。

實務上可以進一步查看錯誤狀態:

error: function(xhr) {
    console.error(xhr.responseText);
    alert('刪除失敗,請稍後再試');
}

18. 整體執行流程

這個頁面的完整流程如下:

  1. 控制器取得指定的產品與圖片。
  2. 控制器回傳 edit.blade.php
  3. Blade 將 $product 資料填入 HTML。
  4. 使用者修改標題、說明、價格或庫存。
  5. 按下「儲存變更」。
  6. 表單以 POST 傳送,並透過 @method('PUT') 告訴 Laravel 這是更新請求。
  7. 控制器進行驗證。
  8. 驗證成功後更新資料庫。
  9. 驗證失敗時返回本頁,並顯示錯誤訊息。
  10. 使用者點擊圖片上的 ×
  11. JavaScript 發送 AJAX DELETE 請求。
  12. Laravel 刪除圖片資料或圖片檔案。
  13. 成功後重新整理頁面。

19. 建議改進

使用 Laravel 路由產生圖片刪除網址

目前寫法是:

url: '/admin/products/images/' + imageId

如果網站部署在子目錄,或未來修改路由,可能造成網址錯誤。可以將路由網址放進 HTML:

<button
    type="button"
    class="delete-image"
    data-url="{{ route('admin.products.images.destroy', $image->id) }}"
    onclick="deleteImage(this.dataset.url)">
    ×
</button>

JavaScript 改成:

function deleteImage(deleteUrl) {
    if (!confirm('確定要刪除這張圖片嗎?')) {
        return;
    }

    $.ajax({
        url: deleteUrl,
        method: 'DELETE',
        headers: {
            'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
        },
        success: function() {
            location.reload();
        },
        error: function(xhr) {
            console.error(xhr.responseText);
            alert('刪除失敗,請稍後再試');
        }
    });
}

為按鈕加上 type="button"

目前按鈕寫成:

<button class="delete-image">

如果它位於表單內,瀏覽器預設可能把它視為 submit。雖然目前圖片區塊位於 <form> 外部,但建議仍明確寫成:

<button type="button" class="delete-image">

避免直接使用 /show-image.php

目前圖片透過:

/show-image.php?path=...

輸出。更 Laravel 化的作法是建立圖片路由:

Route::get('/images/{image}', [ImageController::class, 'show'])
    ->name('images.show');

Blade:

<img src="{{ route('images.show', $image) }}" alt="產品圖片">

這樣可以將權限驗證、檔案存在檢查與路徑安全性集中放在 Controller。

防止圖片路徑漏洞

如果 show-image.php 直接使用使用者傳入的 path,要特別防止目錄穿越攻擊,例如:

../../.env

後端應限制圖片只能從指定目錄讀取,並檢查正規化後的路徑是否仍位於安全目錄內。

不一定需要整頁重新載入

目前刪除成功後:

location.reload();

這個方式簡單可靠,但使用者體驗不是最理想。也可以在成功後直接移除圖片元素:

success: function() {
    $('#image-' + imageId).remove();
}

需要配合 HTML:

<div id="image-{{ $image->id }}" class="image-item">

如此只更新被刪除的圖片,不必重新載入整頁。

因為只能截圖
所以~

提供鋼琴留言版
https://laihao.vaserver.com/gbook

可以實際測試
也歡迎大家一起來學習喔
https://www.pressplay.cc/project/9D489B43D08BD198FDEB03ECDEFAC161/about


上一篇
產品管理列表的修改前端code
下一篇
鋼琴留言版的資料表
系列文
Laravel 讀碼源22
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言